SPB Git forge

spb/cancerindex

Public
37commits 1branches 0releases
2.9 MBsize
maindefault branch
10 days agolast push
TypeScript 97.2% SQL 1.5% CSS 0.6% JavaScript 0.5%
9.8 KB · 190 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { notFound } from 'next/navigation';4import { ExternalLink } from 'lucide-react';5import { PageHeader, Section, KV, Note } from '@/components/ui/section';6import { Badge, StatusBadge } from '@/components/ui/badge';7import { EmptyState } from '@/components/ui/empty-state';8import { Freshness } from '@/components/ui/freshness';9import { getSourceBySlug, listRuns, recordCountsByKind, licenseMeaning, sourceDomains } from '@/lib/queries/sources';10import { fmtDate, fmtDateTime, fmtDuration, fmtInt, humanize, relativeTime } from '@/lib/format';1112export const revalidate = 300;1314export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {15  const s = await getSourceBySlug((await params).slug);16  return s ? { title: `${s.name} — source`, description: s.description ?? `License, access, connector health and record counts for ${s.name}.` } : { title: 'Source' };17}1819export default async function SourcePage({ params }: { params: Promise<{ slug: string }> }) {20  const { slug } = await params;21  const s = await getSourceBySlug(slug);22  if (!s) notFound();23  const [runs, kinds] = await Promise.all([listRuns({ sourceId: s.id, limit: 30 }), recordCountsByKind(s.id)]);24  const domains = [...sourceDomains(s)];2526  return (27    <div>28      <PageHeader kicker={`Source · ${humanize(s.category)}`} title={s.name} lede={s.description ?? undefined}>29        <p className="mt-2 flex flex-wrap items-center gap-2 text-[12.5px]">30          <span className="ci-mono text-ink-3">{s.id}</span>31          <span className="ci-mono text-ink-3">connector: {s.slug}</span>32          <StatusBadge status={s.status} />33          <StatusBadge status={s.health ?? 'unknown'} />34          {s.homepage ? (35            <a className="ci-link inline-flex items-center gap-1" href={s.homepage} target="_blank" rel="noopener noreferrer">36              Homepage <ExternalLink className="h-3 w-3" aria-hidden />37            </a>38          ) : null}39          {s.docs_url ? (40            <a className="ci-link inline-flex items-center gap-1" href={s.docs_url} target="_blank" rel="noopener noreferrer">41              API docs <ExternalLink className="h-3 w-3" aria-hidden />42            </a>43          ) : null}44          {s.terms_url ? (45            <a className="ci-link inline-flex items-center gap-1" href={s.terms_url} target="_blank" rel="noopener noreferrer">46              Terms <ExternalLink className="h-3 w-3" aria-hidden />47            </a>48          ) : null}49        </p>50      </PageHeader>5152      <div className="grid gap-8 lg:grid-cols-[1fr_360px]">53        <div className="space-y-8">54          <Section id="license" kicker="License" title="License and attribution">55            <KV56              items={[57                { k: 'Status', v: <Badge tone={s.license_status === 'approved' ? 'ok' : s.license_status === 'blocked' ? 'danger' : 'warn'}>{s.license_status}</Badge> },58                { k: 'License', v: s.license ?? <span className="text-ink-3">not recorded</span> },59                { k: 'Commercial use', v: humanize(s.commercial_use) },60                { k: 'Redistribution', v: humanize(s.redistribution) },61                { k: 'Reviewed', v: s.license_reviewed_at ? fmtDate(s.license_reviewed_at) : <span className="text-ink-3">not yet reviewed</span> },62                { k: 'Approved for production', v: s.approved_for_production ? 'Yes' : 'No' },63                { k: 'Attribution', v: s.attribution ? <span className="italic">{s.attribution}</span> : <span className="text-ink-3">—</span> },64              ]}65            />66            <Note>{licenseMeaning(s.license_status, s.redistribution, s.commercial_use)}</Note>67          </Section>6869          <Section id="runs" kicker="Connector" title="Ingest runs" description="Every execution of the connector: mode, status, counts, duration and dataset version. Runs never mass-delete on a shrunken response (anomaly guard).">70            {runs.length ? (71              <div className="ci-table-wrap">72                <table className="ci-table">73                  <thead>74                    <tr>75                      <th>Run</th>76                      <th>Mode</th>77                      <th>Status</th>78                      <th>Started</th>79                      <th className="num">Duration</th>80                      <th className="num">Fetched</th>81                      <th className="num">Created</th>82                      <th className="num">Updated</th>83                      <th className="num">Rejected</th>84                      <th className="num">HTTP fail</th>85                      <th>Dataset version</th>86                    </tr>87                  </thead>88                  <tbody>89                    {runs.map((r) => (90                      <tr key={r.id}>91                        <td>92                          <Link href={`/admin/runs/${r.id}`} className="ci-mono ci-link text-[11.5px]">93                            {r.id}94                          </Link>95                          {r.anomaly ? <Badge tone="danger" className="ml-1">anomaly</Badge> : null}96                          {r.schema_drift?.length ? <Badge tone="warn" className="ml-1">drift {r.schema_drift.length}</Badge> : null}97                        </td>98                        <td>{r.mode}</td>99                        <td>100                          <StatusBadge status={r.status} />101                        </td>102                        <td className="whitespace-nowrap text-[12.5px]">{fmtDateTime(r.started_at)}</td>103                        <td className="num">{fmtDuration(r.duration_ms)}</td>104                        <td className="num">{fmtInt(r.records_fetched)}</td>105                        <td className="num">{fmtInt(r.records_created)}</td>106                        <td className="num">{fmtInt(r.records_updated)}</td>107                        <td className="num">{fmtInt(r.records_rejected)}</td>108                        <td className="num">109                          {fmtInt(r.http_failures)}/{fmtInt(r.http_requests)}110                        </td>111                        <td className="ci-mono text-[11.5px]">{r.dataset_version ?? '—'}</td>112                      </tr>113                    ))}114                  </tbody>115                </table>116              </div>117            ) : (118              <EmptyState compact title="No run yet">119                {s.status === 'awaiting_credentials' ? 'The connector is waiting for API credentials.' : s.status === 'review' ? 'The connector is blocked until the license review completes.' : 'This connector has not been executed on this environment.'}120              </EmptyState>121            )}122          </Section>123124          <Section id="records" kicker="Records" title="Source records by entity kind" description="Native records kept for idempotency and lineage (source + entity kind + source record id).">125            {kinds.length ? (126              <div className="ci-table-wrap">127                <table className="ci-table">128                  <thead>129                    <tr>130                      <th>Entity kind</th>131                      <th>Status</th>132                      <th className="num">Records (count)</th>133                      <th>Last retrieved</th>134                    </tr>135                  </thead>136                  <tbody>137                    {kinds.map((k) => (138                      <tr key={`${k.entity_kind}-${k.status}`}>139                        <td className="ci-mono">{k.entity_kind}</td>140                        <td>141                          <StatusBadge status={k.status} />142                        </td>143                        <td className="num">{fmtInt(k.n)}</td>144                        <td className="whitespace-nowrap text-[12.5px]">{k.last_retrieved ? fmtDateTime(k.last_retrieved) : '—'}</td>145                      </tr>146                    ))}147                  </tbody>148                </table>149              </div>150            ) : (151              <EmptyState compact>No record ingested from this source yet.</EmptyState>152            )}153            <Freshness dataUpdatedAt={s.last_success_at} sourceVersion={s.last_dataset_version} />154          </Section>155        </div>156157        <aside className="space-y-8">158          <Section id="access" kicker="Access" title="Connector profile" level={3}>159            <KV160              items={[161                { k: 'Organization', v: s.organization },162                { k: 'Access', v: `${s.access_type} · ${humanize(s.access_auth)}` },163                { k: 'Update frequency', v: s.update_frequency },164                { k: 'Incremental', v: s.supports_incremental ? 'Yes' : 'No (full sync)' },165                { k: 'Rate limit', v: s.rate_limit },166                { k: 'Tier', v: <span className="ci-num">{s.tier}</span> },167                { k: 'Entities', v: s.entities.length ? s.entities.join(', ') : null },168                { k: 'Metrics', v: s.metrics.length ? s.metrics.join(', ') : null },169                { k: 'Domains', v: domains.length ? domains.join(', ') : null },170              ]}171            />172          </Section>173          <Section id="health" kicker="Health" title="Connector health" level={3}>174            <KV175              items={[176                { k: 'Health', v: <StatusBadge status={s.health ?? 'unknown'} /> },177                { k: 'Detail', v: s.health_detail },178                { k: 'Paused', v: s.paused ? 'Yes' : 'No' },179                { k: 'Last success', v: s.last_success_at ? `${relativeTime(s.last_success_at)} (${fmtDateTime(s.last_success_at)})` : '—' },180                { k: 'Last attempt', v: s.last_attempt_at ? fmtDateTime(s.last_attempt_at) : '—' },181                { k: 'Last run', v: s.last_run_id ? <Link className="ci-mono ci-link text-[11.5px]" href={`/admin/runs/${s.last_run_id}`}>{s.last_run_id}</Link> : '—' },182              ]}183            />184          </Section>185        </aside>186      </div>187    </div>188  );189}190